Skip to content

gh-49960: Allow ctypes callbacks to return struct/union types - #157974

Merged
ZeroIntensity merged 10 commits into
python:mainfrom
freakboy3742:ctypes-struct-return
Oct 22, 2026
Merged

ZeroIntensity merged 10 commits into
python:mainfrom
freakboy3742:ctypes-struct-return

Conversation

@freakboy3742

Copy link
Copy Markdown
Contributor

ctypes currently prevents the definition of a callback function that returns a struct or union type.

Unfortunately, it's extremely common for C code to define a callback that returns a structure (e.g. a Point).

Rubicon-ObjC has been carrying a monkypatch for ctypes since 2017 (originally authored by @dgelessus) because there are a small number of macOS Cocoa APIs that return a struct. This PR is a formalisation of that monkey patch, in the hope that we can eventually deprecate the patch.

Based on the historical discussion on gh-49960, struct/union returns were prohibited because of the potential memory implications - a returned struct is passed by value, but if the structure contains a pointer (e.g., a string), the pointer will be copied, but the memory being referenced won't be, which could lead to unexpected results.

The patch doesn't fix that potential memory leak issue - it essentially can't in the general case. Instead, it treats the problem as a user-side issue, handled with documentation. This was the approach suggested by two of the participants on gh-49960.

The patch here is slightly different to the one used in the Rubicon monkeypatch; in Rubicon, we have to patch both getfunc and setfunc because of the residual ctypes handling. By patching the problem in CPython itself, we're able to use setfunc=None as a flag for "non-primitive return type" and use a memory copy.

Fixes #49960.

@read-the-docs-community

read-the-docs-community Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

@bedevere-app bedevere-app Bot added the type-feature A feature request or enhancement label Sep 29, 2026
@encukou

encukou commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

I'm not comfortable reviewing this right now :(
Alas, I never became enough of a ctypes expert to fully understand the keep nuances enough to go against the original author.
I hope that after/if Łukasz's replacement is hired I can get back to features in ctypes.

Meanwhile, @ZeroIntensity is becoming the ctypes expert. Peter, you might want to take a look at this (and e.g. #136876). Try to not make decisions that you'll regret for a long time :)

Whatever design choices are made for this feature, we probably wouldn't be able to undo them after 3.16. And the existence of monkey-patchers means that changes in ctypes internals break more stuff than they “should”.

@ZeroIntensity

Copy link
Copy Markdown
Member

Yeah, I've been slowly working through the backlog of ctypes bugs. I haven't gotten to many type-feature issues yet, but I can put this on my list.

@freakboy3742
freakboy3742 requested review from ZeroIntensity and removed request for encukou September 29, 2026 23:11

@ZeroIntensity ZeroIntensity left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach looks good. A number of nitpicks below.

Comment thread Modules/_ctypes/callbacks.c Outdated
assert(!narrow);
int ok = 0;
if (CDataObject_Check(st, result)) {
int is_inst = PyObject_IsInstance(result, restype_obj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use PyObject_TypeCheck instead of PyObject_IsInstance; the latter can call arbitrary Python code and probably cause some weird crashes.

Suggested change
int is_inst = PyObject_IsInstance(result, restype_obj);
int is_inst = PyObject_TypeCheck(result, restype_obj);

Comment on lines +385 to +386
with warnings.catch_warnings():
warnings.simplefilter("error", RuntimeWarning)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to catch unraisable exceptions that result from this.

Comment thread Modules/_ctypes/callbacks.c Outdated
Comment on lines 281 to 292
if (!ok) {
/* Zero the buffer so the C caller sees deterministic zeros
rather than uninitialised memory. */
memset(mem, 0, restype->size);
PyErr_Format(PyExc_TypeError,
"ctypes callback function returned unexpected "
"type %T", result);
PyErr_FormatUnraisable(
"Exception ignored while converting result "
"of ctypes callback function %R",
callable);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you, but consider dropping this. We already return junk upon failure for the other paths:

>>> from ctypes import CFUNCTYPE, c_int
>>> def evil():
...     return b"test"
...     
>>> print(CFUNCTYPE(c_int)(evil)())
Exception ignored while converting result of ctypes callback function <function evil at 0x7fb36ad345c0>:
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
TypeError: 'bytes' object cannot be interpreted as an integer
1813162336

It would be inconsistent if struct/union returns were deterministic while other types weren't.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being consistent here makes sense to me. Dropped.

PyErr_SetString(PyExc_TypeError,
"invalid result type for callback function");
goto error;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If info->setfunc is NULL, then that means the _fields_ is inherited from a parent class, and thus can be changed later in the subclass. That would mean the size can change, which would cause a crash. Let's lock the fields here:

Suggested change
}
}
if (info->setfunc == NULL) {
// _fields_ must not be assigned after this point
stginfo_set_dict_final(info);
}

Comment thread Lib/test/test_ctypes/test_callbacks.py Outdated
Comment on lines +300 to +301
class SmallRet(Structure):
_fields_ = [("a", c_int), ("b", c_int)]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using the new @ctypes.util.struct utility for structs in this file rather than the ugly old way.

Comment thread Doc/library/ctypes.rst

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add something to the 3.16 whatsnew.

- Check the callback result with PyObject_TypeCheck rather than
  PyObject_IsInstance, so a metaclass __instancecheck__ can't cause a
  smaller buffer to be accepted (an out-of-bounds read).
- Mark the struct/union result type's _fields_ final when the callback
  is created, since the libffi cif depends on its layout.
- Don't zero the result buffer on conversion failure, for consistency
  with other result types.
- Detect unraisable exceptions in the repeated-call test.
- Use ctypes.util.struct for test structures.
- Add a What's New entry.
@freakboy3742

Copy link
Copy Markdown
Contributor Author

@ZeroIntensity I think I've addressed all your feedback; let me know if anything else is needed.

@ZeroIntensity ZeroIntensity left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. I'm still thinking about some of the rough edges here, but I think this is in a state where we can build off of it later. Thanks!

@github-project-automation github-project-automation Bot moved this from Todo to In Progress in Sprint Oct 22, 2026
@ZeroIntensity
ZeroIntensity enabled auto-merge (squash) October 22, 2026 17:44
@ZeroIntensity
ZeroIntensity merged commit eb229a9 into python:main Oct 22, 2026
60 of 62 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Sprint Oct 22, 2026
@freakboy3742
freakboy3742 deleted the ctypes-struct-return branch October 22, 2026 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sprint type-feature A feature request or enhancement

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

ctypes should return composite types from callbacks

3 participants